package se.chalmers.tda366.crumble.ctrl;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Observable;
import java.util.Observer;

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;

import se.chalmers.tda366.crumble.backgroundthreads.DownloadThread;
import se.chalmers.tda366.crumble.core.IArticle;
import se.chalmers.tda366.crumble.core.ICategory;
import se.chalmers.tda366.crumble.core.IFeed;
import se.chalmers.tda366.crumble.core.IModel;
import se.chalmers.tda366.crumble.core.impl.Category;
import se.chalmers.tda366.crumble.core.impl.Feed;
import se.chalmers.tda366.crumble.core.impl.Model;
import se.chalmers.tda366.crumble.gui.CrumbleFrame;
import se.chalmers.tda366.crumble.gui.components.ArticlePreviewPanel;
import se.chalmers.tda366.crumble.gui.components.ArticleViewPanel;
import se.chalmers.tda366.crumble.gui.components.ButtonTabComponent;
import se.chalmers.tda366.crumble.gui.components.FeedButton;
import se.chalmers.tda366.crumble.gui.components.FeedToolBar;
import se.chalmers.tda366.crumble.gui.components.LatestArticlesPanel;
import se.chalmers.tda366.crumble.gui.components.TopArticlesPanel;
import se.chalmers.tda366.crumble.gui.dialogs.AddCategoryView;
import se.chalmers.tda366.crumble.gui.dialogs.AddFeedView;
import se.chalmers.tda366.crumble.gui.dialogs.ConfigureFeedsView;
import se.chalmers.tda366.crumble.gui.dialogs.DialogClosePanel;
import se.chalmers.tda366.crumble.gui.dialogs.GreyoutPanel;
import se.chalmers.tda366.crumble.gui.dialogs.RemoveCategoryView;
import se.chalmers.tda366.crumble.gui.dialogs.RemoveFeedView;
import se.chalmers.tda366.crumble.gui.utils.GuiObserv;
import se.chalmers.tda366.crumble.gui.utils.IArticleLink;
import se.chalmers.tda366.crumble.resources.Constants.Actions;


public class MainController implements PropertyChangeListener, Observer{
	
	private static MainController mainController=null;
	private static IModel model;
	private static final Icon RSS = new ImageIcon(MainController.class.getResource("../images/feed_icon_small.png"));
	private JTabbedPane categoryTabs;
	private DownloadThread dlThread;
	private JPanel frontPagePanel;
	private GreyoutPanel greyPane;
	private CrumbleFrame mainFrame;
	private JLabel statusLabel = new JLabel();
	
	private MainController(){
		initCtrl();
	}
	
	public static MainController getInstance(){
		if (mainController==null)
		{
			mainController=new MainController();
		}
		return mainController;
	}
	
	// Set up when initializing MainController.
	private void initCtrl() {
		// Shutdown hook added to catch close
		// Shutdown hook found on sun: http://java.sun.com/j2se/1.4.2/docs/guide/lang/hook-design.html
		Runtime.getRuntime().addShutdownHook(new Thread() {
		    public void run() { close(); }
		});
		
		model = Model.getInstance();
		model.loadModel();
		
		// Standard set of categories added if no other exists
		if(model.getCategories().size() == 0)
		{
			model.addCategory(new Category("News"));
			model.addCategory(new Category("Business"));
			model.addCategory(new Category("Entertainment"));
			model.addCategory(new Category("Sports"));
			model.addCategory(new Category("Other"));
		}
		GuiObserv.getInstance().addObserver(this);
	}
	
	// Start the background download thread
	public void initDownload() {
		ArrayList<IFeed> feeds = new ArrayList<IFeed>();
		for(ICategory category : model.getCategories())
		{
			feeds.addAll(category.getFeeds());
		}
		dlThread = new DownloadThread(feeds);
		dlThread.addPropertyChangeListener(this);
		dlThread.start();
	}	
	
	// Adds a category to model. Also calls addCategoryTab to add the tab
	private void addCategory(String name) {
		ICategory newCat = new Category(name);
		model.addCategory(newCat);
		closeDialog();
		addCategoryTab(newCat,-1);
	}
	
	// Adds the category tab. If index is set to -1 it will be added to the end of the categories
	// otherwise to the index. (this i used also by feed tabs)
	private void addCategoryTab(ICategory category, int index) {
		if(index == -1)
		{
			//first index is front page
			index = model.getCategories().size();
		}
		JPanel panel = new JPanel();
		category.setPanel(panel);
		//categoryTabs.addTab(category.getName(), panel);
		categoryTabs.insertTab(category.getName(), null, panel, category.getName(), index);
	}
	
	// adds a feed and closes download, restarts download and updates gui
	private void addFeed(URL adress, String name, ICategory category) {
		category.addFeed(createFeed(adress, name));
		closeDialog();
		restartDownload();
		updateAll();
	}
	
	// changes the category of the feed to the new cat
	private void changeCategory(IFeed feed, ICategory category) {
		for(ICategory allCats : model.getCategories())
		{
			allCats.getFeeds().remove(feed);
		}
		category.addFeed(feed);
	}
	
	// called by the shutdown hook. Saves feeds, frame position and frame dimension
	private void close() {
		System.out.println("CLOSING SYSTEM");
		model.saveModel();
		System.out.println("FEEDS SAVED");
		saveMainFrameDimension();
		System.out.println("DIMENSION SAVED");
		saveMainFramePosition();
		System.out.println("POSITION SAVED");
		System.out.println("SYSTEM STATE SAVED");
	}
	
	// closes the dialog and greyout panel
	private void closeDialog() {
        greyPane.setVisible(false);
    }
	
	// configures the feed. if category = null, category is not changed.
	private void configureFeed(URL adress, String name, IFeed feed, ICategory category) {
		feed.setSource(adress.toString());
		feed.setName(name);
		if(category != null)
		{
			changeCategory(feed,category);
			categoryTabs.setSelectedComponent(category.getPanel());
		}
		closeDialog();
		updateAll();
		
	}
	
	// creates a feed from an URL and a name. Adds feed to downloadthread
	private IFeed createFeed(URL url, String name){
		IFeed newFeed = new Feed (url.toString());
		newFeed.setName(name);
		dlThread.addFeed(newFeed);
		return newFeed;
	}
	
	// lops throught all categories and compiles a list of all feeds.
	private List<IFeed> getAllFeeds() {
		List<IFeed> feeds = new ArrayList<IFeed>();
		for(ICategory category : model.getCategories())
		{
			feeds.addAll(category.getFeeds());
		}
		return feeds;
	}
	
	// fetches the categories from model
	private List<ICategory> getCategories() {
		return model.getCategories();
	}
	
	// returns a list of the latest articles. Amount is the max lenght.
	// inCat is set if latest should be only from one category, if null, all categories are used.
	private ArrayList<IArticle> getLatestArticles(int amount, ICategory inCat) {
		List<ICategory> categories = new ArrayList<ICategory>();
		if(inCat == null)
		{
			categories = model.getCategories();
		}
		else
		{
			categories.add(inCat);
		}
		ArrayList<IArticle> list = new ArrayList<IArticle>();
		for(ICategory category : categories){
			for(IFeed feed : category.getFeeds()){
				for(IArticle article : feed.getArticles()){
					list.add(article);
				}
			}
		}
		Collections.sort(list);
		if(list.size()<=amount){
			return list;
		}
		return new ArrayList<IArticle>(list.subList(0, amount));
	}
	
	// returns a list of the top articles. Amount is the max lenght.
	// top articles are the first from each feed, then the second etc.
	// inCat is set if top should be only from one category, if null, all categories are used.	
	private ArrayList<IArticle> getTopArticles(int amount, ICategory inCat) {
		List<ICategory> categories = new ArrayList<ICategory>();
		if(inCat == null)
		{
			categories = model.getCategories();
		}
		else
		{
			categories.add(inCat);
		}
		ArrayList<IArticle> topArts = new ArrayList<IArticle>();
		int level = 0;
		for(int x=0;topArts.size()<amount && x<amount;x++)
		{
			for(ICategory category : categories)
			{
				for(IFeed feed : category.getFeeds())
				{
					if(feed.getArticles().size()>level)
					{
						topArts.add(feed.getArticles().get(level));
					}
					if(topArts.size() >= amount)
						break;
				}
				if(topArts.size() >= amount)
					break;
			}
			level++;
		}
		return topArts;
	}
	
	// loads the mainframe dimension from user home if it has been set
	public Dimension loadMainFrameDimension() {
		Dimension dimension = new Dimension(1050,800);
		try {
            InputStream file = new FileInputStream(new File(System.getProperty("user.home") + "/.crumbleApp/user_mfDim.crum"));
            InputStream buffer = new BufferedInputStream(file);
            ObjectInput input = new ObjectInputStream(buffer);

            try {
            	dimension = (Dimension)input.readObject();
            }
            finally {
                input.close();
            }
        }
        catch (ClassNotFoundException e) {
            System.err.println("ClassNotFoundException!");
        }
        catch (IOException e) {
            System.err.println("Unable to load dimension!");
        }
        return dimension;
	}
	
	//loads the mainframe position from user home if it has been set
	public Point loadMainFramePosition() {
		Point point = null;
		try {
            InputStream file = new FileInputStream(new File(System.getProperty("user.home") + "/.crumbleApp/user_mfPos.crum"));
            InputStream buffer = new BufferedInputStream(file);
            ObjectInput input = new ObjectInputStream(buffer);

            try {
            	point = (Point)input.readObject();
            }
            finally {
                input.close();
            }
        }
        catch (ClassNotFoundException e) {
            System.err.println("ClassNotFoundException!");
        }
        catch (IOException e) {
            System.err.println("Unable to load position!");
        }
        return point;
	}
	
	// property change for download thread to notify controller status change
	@SuppressWarnings("deprecation")
	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		if(evt.getPropertyName().equals("DLFIN"))
		{
			System.out.println("Property change fired");
			if((Integer)evt.getNewValue() == 1)
			{
				updateAll();
				Date now = new Date();
				statusLabel.setText("Feeds last updated at "+now.getHours()+":"+se.chalmers.tda366.crumble.utils.DecimalFormatCollection.MINUTES.format(now.getMinutes()));
				System.out.println("Download finished.");
			}
			else if((Integer)evt.getNewValue() == 0)
			{
				statusLabel.setText("Updating feeds in background...");
				System.out.println("Download started.");
			}
		}
    }
	
	// removes the category from model
	private void removeCategory(ICategory category) {
		model.removeCategory(category);
		updateCategoryTabs();
		closeDialog();
	}
	
	// removes the feed from categories.
	private void removeFeed(IFeed remFeed) {
		for(ICategory category : model.getCategories())
		{
			category.getFeeds().remove(remFeed);
		}
		updateAll();
		closeDialog();
	}
	
	// repaints the greyout dialog. called when resizing frame
	private void repaintDialog() {
        if(greyPane.isVisible())
        {
            showDialog();
        }
    }

	// interupts the threads sleep and starts download
	private void restartDownload() {
		dlThread.interrupt();
	}
	
	// saves the mainframe dimension on shutdown to user home
	private void saveMainFrameDimension() {
		//Try to save the feeds to disc.
		try {
			new File(System.getProperty("user.home") + "/.crumbleApp/").mkdir();
            OutputStream file = new FileOutputStream(new File(System.getProperty("user.home") + "/.crumbleApp/user_mfDim.crum"));
            OutputStream buffer = new BufferedOutputStream(file);
            ObjectOutput output = new ObjectOutputStream(buffer);

            try {
                output.writeObject(mainFrame.getSize());
            }
            finally {
                output.close();
            }
        }
        catch (IOException e) {
        	System.err.println("Unable to save mainframe dimension.");
        }
	}
	
	// saves the mainframe position on shutdown to user home
	private void saveMainFramePosition() {
		//Try to save the feeds to disc.
		try {
			new File(System.getProperty("user.home") + "/.crumbleApp/").mkdir();
            OutputStream file = new FileOutputStream(new File(System.getProperty("user.home") + "/.crumbleApp/user_mfPos.crum"));
            OutputStream buffer = new BufferedOutputStream(file);
            ObjectOutput output = new ObjectOutputStream(buffer);

            try {
                output.writeObject(mainFrame.getLocation());
            }
            finally {
                output.close();
            }
        }
        catch (IOException e) {
        	System.err.println("Unable to save mainframe position.");
        }
	}
	
	// gives the controller access to the tabbed pane for categories
	public void setCategoryTabs(JTabbedPane categoryTabs) {
		this.categoryTabs = categoryTabs;
	}
	
	// gives the controller access to the frontpagepanel
	public void setFrontPagePanel(JPanel frontPagePanel) {
		this.frontPagePanel = frontPagePanel;
	}

	// gives the controller access to the mainframe
    public void setMainFrame(CrumbleFrame mainFrame) {
		this.mainFrame = mainFrame;
		greyPane = new GreyoutPanel();
		mainFrame.setGlassPane(greyPane);
		mainFrame.addComponentListener(new ComponentListener()
			{
				@Override
				public void componentHidden(ComponentEvent arg0) {
					// TODO Auto-generated method stub	
				}
	
				@Override
				public void componentMoved(ComponentEvent arg0) {
					// TODO Auto-generated method stub				
				}
	
				@Override
				public void componentResized(ComponentEvent arg0) {
					MainController.getInstance().repaintDialog();
				}
	
				@Override
				public void componentShown(ComponentEvent arg0) {
					// TODO Auto-generated method stub
				}
			}
		);
	}
    
    // gives the controller access to the statuslabel
    public void setStatusLabel(JLabel statusLabel) {
		this.statusLabel = statusLabel;
	}
    
    // creates addCategoryView and sets it to greypane.
    private void showAddCategoryDialog() {
		AddCategoryView addCategory = new AddCategoryView();
		greyPane.setInnerPanel(addCategory);
		greyPane.setPaintDimension(addCategory.getPreferredSize());
		showDialog();
	}

    // creates addFeedView and sets it to greypane
    private void showAddFeedDialog() {
		AddFeedView addFeed = new AddFeedView(getCategories());
		greyPane.setInnerPanel(addFeed);
		greyPane.setPaintDimension(addFeed.getPreferredSize());
		showDialog();
	}
	
    // creates configureFeedsView and sets it to greypane if feeds are available.
    private void showConfigureFeedsDialog() {
		List<IFeed> feeds = getAllFeeds();
		if(feeds.size()>0)
		{
			ConfigureFeedsView configureFeeds = new ConfigureFeedsView(feeds, model.getCategories());
			greyPane.setInnerPanel(configureFeeds);
			greyPane.setPaintDimension(configureFeeds.getPreferredSize());
			showDialog();	
		}
	}
	
    // shows the greyout dialog with inner panel
    private void showDialog() {
        greyPane.setVisible(false);
        greyPane.removeAll();
        JPanel closePanel = new DialogClosePanel();
        closePanel.setBounds(
                (int)(mainFrame.getSize().getWidth()/2-greyPane.getInnerPanel().getPreferredSize().getWidth()/2),
                (int)(mainFrame.getSize().getHeight()/2-greyPane.getPaintDimension().getHeight()/2-closePanel.getPreferredSize().getHeight()),
                (int)greyPane.getInnerPanel().getPreferredSize().getWidth(),
                (int)closePanel.getPreferredSize().getHeight());
        greyPane.setLayout(null);
        greyPane.getInnerPanel().setBounds(
                (int)(mainFrame.getSize().getWidth()/2-greyPane.getInnerPanel().getPreferredSize().getWidth()/2),
                (int)(mainFrame.getSize().getHeight()/2-greyPane.getPaintDimension().getHeight()/2),
                (int)greyPane.getInnerPanel().getPreferredSize().getWidth(),
                (int)greyPane.getInnerPanel().getPreferredSize().getHeight());
        greyPane.add(closePanel);
        greyPane.add(greyPane.getInnerPanel());

        greyPane.setVisible(true);
        mainFrame.repaint();
    }

    // creates a tab for specific feed and adds it last in the tab pane.
    private void showFeed(IFeed feed) {
    	JPanel feedPanel = new JPanel();
    	for(IArticle article : feed.getArticles())
		{
			ArticlePreviewPanel artPrev = new ArticlePreviewPanel(article);
			feedPanel.add(artPrev);
		}
    	categoryTabs.addTab(feed.getName(), RSS, feedPanel);
    	categoryTabs.setTabComponentAt(categoryTabs.getTabCount()-1, new ButtonTabComponent(categoryTabs, RSS));
    	categoryTabs.setBackgroundAt(categoryTabs.getTabCount()-1, Color.WHITE); 
    	categoryTabs.setSelectedIndex(categoryTabs.getTabCount()-1);
    }

    // creates a removeCategoryView and sets it to graypane
    private void showRemoveCategoryDialog() {
		RemoveCategoryView removeCategory = new RemoveCategoryView(getCategories());
		greyPane.setInnerPanel(removeCategory);
		greyPane.setPaintDimension(removeCategory.getPreferredSize());
		showDialog();
	}
	
    // creates a removeFeedView and sets it to greypane
    private void showRemoveFeedDialog() {
		List<IFeed> feeds = getAllFeeds();
		if(feeds.size()>0){
			RemoveFeedView removeFeed = new RemoveFeedView(feeds);
			greyPane.setInnerPanel(removeFeed);
			greyPane.setPaintDimension(removeFeed.getPreferredSize());
			showDialog();
		}
	}

    // update from Observer to handle all actions from gui
	@Override
	public void update(Observable source, Object a) {
		Actions action = (Actions) a;
		
		// all sources are GuiObserv to allow access to panels and fields.
		GuiObserv guiSource = (GuiObserv) source;
		
		switch(action){
			case ADD_CATEGORY:
				addCategory(((AddCategoryView)guiSource.getEventSource()).getCategory());
				break;
			case ADD_FEED:
				AddFeedView addFeedView = (AddFeedView)guiSource.getEventSource();
				addFeed(addFeedView.getFeedURL(), addFeedView.getFeedName(), addFeedView.getCategory());
				break;
			case CLOSE_DIALOG:
				closeDialog();
				break;
			case EDIT_FEED:
				ConfigureFeedsView configureFeedsView = (ConfigureFeedsView) guiSource.getEventSource();
				ICategory editCat = null;
				if(configureFeedsView.getCategoryComboBox().getSelectedIndex() != 0)
				{
					editCat = (ICategory) configureFeedsView.getCategoryComboBox().getSelectedItem();
				}
				configureFeed(configureFeedsView.getUrl(), configureFeedsView.getNewName(),
							configureFeedsView.getFeed(), editCat);
				break;
			case EXIT_BUTTON:
				closeDialog();
				break;
			case REFRESH_FEEDS:
				restartDownload();
				break;
			case REMOVE_CATEGORY:
				removeCategory(((RemoveCategoryView) guiSource.getEventSource()).getCategory());
				break;
			case REMOVE_FEED:
				removeFeed(((RemoveFeedView) guiSource.getEventSource()).getFeed());
				break;
			case SHOW_ADD_CATEGORY_DIALOG:
				showAddCategoryDialog();
				break;
			case SHOW_ADD_FEED_DIALOG:
				showAddFeedDialog();
				break;
			case SHOW_ARTICLE:
				viewArticle(((IArticleLink) guiSource.getEventSource()).getArticle());
				break;
			case SHOW_ARTICLE_IN_BROWSER:
				//showArticleInBrowser(uri);
				break;
			case SHOW_CONFIGURE_FEEDS_DIALOG:
				showConfigureFeedsDialog();
				break;
			case SHOW_FEED:
				showFeed(((FeedButton) guiSource.getEventSource()).getFeed());
				break;
			case SHOW_REMOVE_CATEGORY_DIALOG:
				showRemoveCategoryDialog();
				break;
			case SHOW_REMOVE_FEED_DIALOG:
				showRemoveFeedDialog();
				break;
			default:
				System.out.println("Default");
				break;
		}		
	}

	// update front page and categories
	public void updateAll() {
		updateFrontPage();
		updateAllCategories();
	}

	// update all categories
	private void updateAllCategories() {
		for(ICategory category : model.getCategories())
		{
			updateCategory(category);
		}
	}

	// update the category with articles and top lists.
	private void updateCategory(ICategory category) {
		JPanel categoryPanel = category.getPanel();
		if(categoryPanel != null)
		{
			ArrayList<IArticle> articles;
			articles = getTopArticles(20,category);
			categoryPanel.removeAll();
			categoryPanel.setLayout(new BorderLayout());
			JPanel categoryNewsContainer = new JPanel();
			JPanel eastContainer = new JPanel();
			FeedToolBar feedToolBar = new FeedToolBar(category.getFeeds());
			categoryPanel.add(feedToolBar, BorderLayout.NORTH);
			categoryPanel.add(categoryNewsContainer, BorderLayout.CENTER);
			categoryPanel.add(eastContainer, BorderLayout.EAST);
			LatestArticlesPanel latestArtPan = new LatestArticlesPanel(getLatestArticles(10,category));
			TopArticlesPanel topArtPan = new TopArticlesPanel(getTopArticles(10,category));
			eastContainer.setPreferredSize(new Dimension(300,mainFrame.getSize().height));
			eastContainer.add(latestArtPan);
			eastContainer.add(topArtPan);
			for(IArticle article : articles)
			{
				ArticlePreviewPanel artPrev = new ArticlePreviewPanel(article);
				categoryNewsContainer.add(artPrev);
			}
			categoryPanel.repaint();
			mainFrame.repaint();
		}
	}

	// update the category tabs from model
	public void updateCategoryTabs() {
		categoryTabs.removeAll();
		frontPagePanel = new JPanel();
		categoryTabs.addTab("Front page", frontPagePanel);
		//first is for frontpage
		int x = 1;
		for(ICategory category : model.getCategories())
		{
			addCategoryTab(category,x);
			x++;
		}
		updateAll();
	}

	// update the front page with articles and top lists
	private void updateFrontPage() {
		if(frontPagePanel != null)
		{
			ArrayList<IArticle> articles;
			articles = getTopArticles(20,null);
			frontPagePanel.removeAll();
			frontPagePanel.setLayout(new BorderLayout());
			JPanel frontPageNewsContainer = new JPanel();
			JPanel eastContainer = new JPanel();
			frontPagePanel.add(frontPageNewsContainer, BorderLayout.CENTER);
			frontPagePanel.add(eastContainer, BorderLayout.EAST);
			LatestArticlesPanel latestArtPan = new LatestArticlesPanel(getLatestArticles(10,null));
			TopArticlesPanel topArtPan = new TopArticlesPanel(getTopArticles(10,null));
			eastContainer.setPreferredSize(new Dimension(300,mainFrame.getSize().height));
			eastContainer.add(latestArtPan);
			eastContainer.add(topArtPan);
			for(IArticle article : articles)
			{
				ArticlePreviewPanel artPrev = new ArticlePreviewPanel(article);
				frontPageNewsContainer.add(artPrev);
				artPrev.setVisible(true);
			}
			frontPagePanel.repaint();	
		}
	}
	
	// view a specific article in articleView. Is displayed in the greyout dialog.
	private void viewArticle(IArticle article) {
		ArticleViewPanel artPan = new ArticleViewPanel(article);
		artPan.setVisible(true);
		artPan.setPreferredSize(
				new Dimension(((int)(mainFrame.getSize().width*0.6)),
						((int)(mainFrame.getSize().height*0.8))));
		greyPane.setInnerPanel(artPan);
		greyPane.setPaintDimension(artPan.getPreferredSize());
		showDialog();
	}
	
}
